package roborally.board;

import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Random;
import java.util.Set;

import roborally.itemsonboard.BoardItem;
import roborally.supporting.DefaultConstants;
import roborally.supporting.Position;
import roborally.supporting.Terminatable;

import exception.OverFlowException;

import be.kuleuven.cs.som.annotate.*;

/**
 * A class representing a 2 dimensional board. A board can contain objects. Not
 * all combinations of objects can be on the same place. Boards can be merged
 * together. When a board is terminated, all its objects it contains are
 * terminated.
 * 
 * @invar The boardItems must be valid.
 * 		| hasProperItems()
 * @invar The xLimit must be valid.
 * 		| isValidXLimit(this.getXLimit())
 * @invar The yLimit must be valid.
 * 		| isValidYLimit(this.getYLimit())
 * 
 * @author Molderez Tom, 2de bach Ingenieurswetenschappen, Computerwetenschappen-Elektrotechniek and Reniers Jorn, 2de bach Ingenieurswetenschappen Werktuigkunde-Computerwetenschappen
 * @version 3.0
 * 
 */
public class Board implements Terminatable {
	
	/**
	 * Creates a new board with given options and no boardItems on it.
	 * 
	 * @param xLimit The x limit of the board.
	 * @param yLimit The y limit of the board.
	 * @post The xLimit of this board is equal to the given xLimit.
	 * 		| (this new).getXLimit() == xLimit
	 * @post The yLimit of this board is equal to the given yLimit.
	 * 		| (this new).getYlimit() == yLimit
	 * @throws IllegalArgumentException
	 * 		When the xLimit or the yLimit are not valid.
	 * 		| !isValidXLimit(xLimit) || !isValidYLimit(yLimit)
	 */
	public Board(long xLimit, long yLimit) throws IllegalArgumentException {
		if (!isValidXLimit(xLimit) || !isValidYLimit(yLimit))
			throw new IllegalArgumentException("the limits are invalid");
		this.xLimit = xLimit;
		this.yLimit = yLimit;
	}

	/**
	 * Creates a new board with xLimit and yLimit equal to 10.
	 * 
	 * @effect Creates a new board with xLimit and yLimit equal to 10.
	 * 		| this(10L,10L)
	 */
	public Board() {
		this(10L, 10L);
	}

	/**
	 * Terminates this board.
	 * 
	 * By terminating this board, all the boardItems on the board will be terminated as well. Moreover, the boardItems will
	 * be removed from the board.
	 * @effect Terminates all boardItems on this board.
	 * 		| for each position in {getPositionsInUse()} do
	 * 		|	for each boardItem in {getItemsAt(position)} do
	 * 		|		boardItem.setBoard(null)
	 * 		| 		boardItem.terminate()
	 * @post The board contains no more boardItems.
	 * 		| (new this).getPositionsInUse().isEmpty()
	 * @post The board will be terminated.
	 * 		| (new this).isTerminated() == true
	 */
	@Override
	public void terminate() {		
		for (Position keyPosition : getPositionsInUse())
			for (BoardItem lusBoardItem : this.getItemsAt(keyPosition)) {
				lusBoardItem.setBoard(null);
				lusBoardItem.terminate();
			}
		items.clear();
		this.isTerminated = true;
	}
	
	/**
	 * Returns whether this board is terminated.
	 */
	@Override @Basic @Raw
	public boolean isTerminated(){
		return this.isTerminated;
	}
	
	/**
	 * A boolean indicating whether this board is terminated.
	 */
	private boolean isTerminated = false;

	/**
	 * Controls if the given xLimit is valid.
	 * 
	 * @param xLimit The xLimit to be controlled.
	 * @return The xLimit must be greater or equal to 0 and smaller or equal to the default board x upper limit.
	 * 		| xLimit >= 0 && xLimit <= DefaultConstants.BOARD_UPPER_LIMIT_x
	 */
	@Raw
	public static boolean isValidXLimit(long xLimit) {
		return xLimit >= 0 && xLimit <= DefaultConstants.BOARD_UPPER_LIMIT_X;
	}

	/**
	 * Returns the xLimit of the board.
	 */
	@Basic @Raw @Immutable
	public long getXLimit() {
		return xLimit;
	}

	/**
	 * The xLimit of this board. 
	 */
	private final long xLimit;

	/**
	 * Controls if the given yLimit is valid.
	 * 
	 * @param yLimit The yLimit to be controlled.
	 * @return The yLimit must be greater or equal to 0 and smaller or equal to the default board y upper limit.
	 * 		| yLimit >= 0 && yLimit <= DefaultConstants.BOARD_UPPER_LIMIT_Y
	 */
	@Raw
	public static boolean isValidYLimit(long yLimit) {
		return yLimit >= 0 && yLimit <= DefaultConstants.BOARD_UPPER_LIMIT_Y;
	}

	/**
	 * Returns the yLimit of this board.
	 */
	@Basic @Raw @Immutable
	public long getYLimit() {
		return yLimit;
	}

	/**
	 * The yLimit of this board.
	 */
	private final long yLimit;


	/**
	 * Checks whether this position fits on the board.
	 * 
	 * @param position
	 *            The position to be controlled.
	 * @return | result == position.getX() <= getXLimit() && position.getY() <= getYLimit()
	 */
	public boolean canHaveAsPosition(Position position) {
		return position.getX() <= getXLimit() && position.getY() <= getYLimit();
	}

	/**
	 * Controls if the board can contain the boardItem (at the position of the boardItem)
	 * 
	 * @param boardItem The boardItem to be controlled.
	 * @return If the position is null, return false.
	 * 		| if boardItem == null
	 * 		|   then result == false
	 * @return If the position or the board is terminated, return false.
	 * 		| if boardItem.isTerminated() || this.isTerminated()
	 * 		| 	then result == false
	 * @return If the board can not contain an item on the position of the boardItem, return false.
	 * 		| if !canHaveAsPosition(boardItem.getPosition())
	 * 		|	then result == false
	 * @return If at the specific position of the boardItem, the board can not contain the boardItem
	 * 			because of other items, return false. Else return true.
	 * 		| let temp = true
	 * 		| for each item in {getItemsAt(boardItem.getPosition())}
	 * 		| 	if(!item.canHaveItemOnSamePosition(item)
	 * 		|		then temp = false
	 * 		| result == temp
	 */
	@Raw
	public boolean canHaveAsItem(BoardItem boardItem) {
		if (boardItem == null)
			return false;
		if (boardItem.isTerminated() || this.isTerminated())
			return false;
		if (!canHaveAsPosition(boardItem.getPosition()))
			return false;
		for (BoardItem item : getItemsAt(boardItem.getPosition())) {
			if (!item.canHaveItemOnSamePosition(boardItem))
				return false;
		}
		return true;
	}

	/**
	 * Controls if this board contains valid items.
	 * 
	 * @return For every item on this board, the board must can have the particular item and 
	 * 			the item must have this as board.
	 * 		| let temp = true 
	 * 		| for each position in {getPositionsInUse()} do
	 * 		|	for each boardItem in {this.getItemsAt(position)} do
	 * 		|      if !canHaveAsItem(boardItem) || lusboardItem.getBoard() != this || !position.equals(boardItem.getPosition())
	 * 		|		 then temp = false
	 * 		| result == temp
	 */
	public boolean hasProperItems() {
		for (Position keyPosition : getPositionsInUse()) {
			for (BoardItem lusboardItem : this.getItemsAt(keyPosition)) {
				if (!canHaveAsItem(lusboardItem))
					return false;
				if (lusboardItem.getBoard() != this)
					return false;
				if (!keyPosition.equals(lusboardItem.getPosition()))
					return false;
			}
		}
		return true;
	}

	/**
	 * Add a item to the board if it is valid.
	 * 
	 * @param item The item to be added to the board.
	 * @post The item is placed onto the board at his position.
	 * 		| (new this).getItemsAt(item.getPosition()).contains(item)
	 * @effect The item has this as board.
	 * 		| item.setBoard(this)
	 * @throws IllegalArgumentException
	 * 		When the item is already on a board or the board can not contain this item.
	 * 		| item.getBoard() == null || !canHaveAsItem(item)
	 */
	public void addAsItem(BoardItem item) throws IllegalArgumentException {
		if (!canHaveAsItem(item))
			throw new IllegalArgumentException("Can not contain this item");
		if (item.getBoard() != null)
			throw new IllegalArgumentException("This item has already a board");
		else {
			if (items.get(item.getPosition()) != null)
				items.get(item.getPosition()).add(item);
			else {
				HashSet<BoardItem> temp = new HashSet<BoardItem>();
				temp.add(item);
				items.put(item.getPosition(), temp);
			}
			item.setBoard(this); // this is here Raw because item is on the board, but the board of this item is not this board. See setBoard
		} //when it leaves this method, everything satisfies its classinvariants.
	}

	/**
	 * Removes the particular item from the board.
	 * 
	 * @param item The item to be removed
	 * @effect The item does not contain this board anymore.
	 * 		| item.setBoard(null)
	 * @post The board does not contain this item anymore.
	 * 		| (new this).getItemsAt(item.getPosition).contains(item)==false
	 * @throws IllegalArgumentException
	 * 		When the board does not contain the particular item.
	 * 		| this.getItemsAt(item.getPosition()).contains(item)==false
	 */
	public void removeAsItem(BoardItem item) throws IllegalArgumentException {
			if (!this.getItemsAt(item.getPosition()).contains(item))
				throw new IllegalArgumentException("Item is not onto this board");
			else {
				items.get(item.getPosition()).remove(item);
				item.setBoard(null);
				if (this.getItemsAt(item.getPosition()).isEmpty())
					items.remove(item.getPosition());
			}
	}
	
	/**
	 * Returns a random item at the given position if the position contains an item.
	 * 
	 * @param position The position at which a random item must be removed.
	 * @pre There must be at least one boardItem at the given position.
	 * 		| !getItemsAt(position).isEmpty()
	 * @post Result is a random boardItem at the given position.
	 * 		| let x be a random number in (0..getItemsAt(position).size()-1) (borders included)
	 * 		| let boardItems = getItemsAt(position).toArray(new boardItem[0])
	 * 		| result == boardItems[x]
	 */
	public BoardItem getRandomAtPosition(Position position) {
		assert !getItemsAt(position).isEmpty();
		BoardItem[] boardItems = getItemsAt(position).toArray(new BoardItem[0]);
		Random randomGenerator = new Random();
		return boardItems[randomGenerator.nextInt(boardItems.length)];
	}

	/**
	 * Return the total number of items on this board.
	 * 
	 * @return The total number of items on this board.
	 * 		| let totalItems = 0L
	 * 		| for each position in {getPositionsInUse()} do
	 * 		|	totalItems += getNbItemsAt(position)
	 * 		| result == totalItems
	 * @throws OverFlowException
	 * 		When there are to many items on a board to represent by a long.
	 * 		| let totalItems = 0L
	 * 		| for each position in {getPositionsInUse()} do
	 * 		|	totalItems += getNbItemsAt(position)
	 * 		|	if totalItems < 0
	 */
	public long getNbItems() throws OverFlowException {
		long totalItems = 0L;
		for (Position keyPosition : getPositionsInUse()) {
			totalItems += getNbItemsAt(keyPosition);
			if (totalItems < 0)
				throw new OverFlowException(Long.class,
						"to many items on the board for a long");
		}
		return totalItems;
	}

	/**
	 * Return the amount of items on this position.
	 * 
	 * @param position The position of which the items must be count.
	 * @return The number of items on the given position.
	 * 		| result == this.getItemsAt(position).size()
	 */
	public int getNbItemsAt(Position position) {
		return this.getItemsAt(position).size();
	}
	
	/**
	 * Returns a set containing all the items on the given position.
	 * 
	 * @param position The position of which items must be returned.
	 */
	@Basic
	public Set<BoardItem> getItemsAt(Position position) {
		if(items.get(position) == null)
			return Collections.unmodifiableSet(new HashSet<BoardItem>());
		else
			return Collections.unmodifiableSet(items.get(position));
	}
	
	/**
	 * Returns a set containing all the positions in use.
	 */
	@Basic
	public Set<Position> getPositionsInUse() {
		return items.keySet();
	}
	
	/**
	 * Returns a set containing only items of the given class or items that extends the 
	 * given class.
	 * 
	 * @param T The class that implements BoardItem, of which items of this board must be returned.
	 * @return Each item that is placed onto this and has the proper type is returned.
	 * 		| for each position in {getPositionsInUse()}
	 * 		|	for each boardItem in {getItemsAt(position)}
	 * 		|		if T.isInstance(boardItem)
	 * 		|			then result.contains(boardItem)
	 */
	public <T extends BoardItem> Set<T> getAllItemsType(Class<T> T) {
		Set<T> itemsTypeT = new HashSet<T>();
		for(Position lusPosition : getPositionsInUse())
			itemsTypeT.addAll(getItemsAtType(T,lusPosition));
		return itemsTypeT;
	} 
	
	/**
	 * Returns a set containing only items of the given class or items that extends the 
	 * given class at the given position.
	 * 
	 * @param T The class that implements BoardItem, of which items of this board must be returned.
	 * @param position The position of which objects must be returned.
	 * @return Each item that is placed onto this and has the proper type is returned.
	 * 		|for each boardItem in {getItemsAt(position)}
	 * 		|		if T.isInstance(boardItem)
	 * 		|			then result.contains(boardItem)
	 */
	public <T extends BoardItem> Set<T> getItemsAtType(Class<T> T, Position position) {
		Set<T> itemsTypeT = new HashSet<T>();
		for(BoardItem lusBoardItem : getItemsAt(position))
			if(T.isInstance(lusBoardItem))
				itemsTypeT.add(T.cast(lusBoardItem));
		return itemsTypeT;
	}
	
	/**
	 * A map that stores for each position in use a set containing all the boardItems on that particular position.
	 */
	private final Map<Position, HashSet<BoardItem>> items = new HashMap<Position, HashSet<BoardItem>>();

	/**
	 * Merges two board. All the items of board are transferred to this board, if possible to exactly the same position, 
	 * else to a position next to ("alongside") the original position. If that isn't possible either, they are terminated. The board is
	 * also terminated.
	 * 
	 * @param board The board of which the items must be set to this board, and that must be terminated.
	 * @pre Board may not be null.
	 * 		| board != null
	 * @pre Board may not be terminated.
	 * 		| !board.isTerminated()
	 * @pre This may not be terminated
	 * 		| !this.isTerminated()
	 * @effect
	 * 		| for each position in {board.getPositionsInUse}
	 * 		| 	for each boardItem in {board.getItemsAt(boardItem)}
	 * 		|		board.removeAsItem(boardItem)
	 * 		|		if this.canHaveAsItem(boardItem)
	 * 		|			then this.addAsItem(boardItem)
	 * 		|		else
	 * 		|			then board.addAsItem(boardItem)	
	 * @effect
	 * 		| board.terminate()
	 */
	public void merge(Board board) {
		assert board != null;
		assert !board.isTerminated();
		assert !this.isTerminated();
		//get all object of the board
		Set<BoardItem> itemsOfBoard = new HashSet<BoardItem>();
		for(Position lusPosition : board.getPositionsInUse()) {
			itemsOfBoard.addAll(board.getItemsAt(lusPosition));
		}
		//try to place all object of board on this
		for(BoardItem lusboardItem : itemsOfBoard) {
			board.removeAsItem(lusboardItem);
			if(this.canHaveAsItem(lusboardItem))
				this.addAsItem(lusboardItem);
			else
				board.addAsItem(lusboardItem);
		}
		//terminating board together with all the objects that could not be places on this board.
		board.terminate();
	}
	
	/**
	 * Returns a textual specification of this board.
	 * 
	 * @return A textual specification of this board.
	 * 		| result ==this.getClass().getSimpleName() + ": [LimitX = " + this.getXLimit() + "], [LimitY = " + this.getYLimit() + "], [Total items = " + this.getNbItems() + "], " +
	 *		|				"[Legal status = " + this.hasProperItems() + "], [Items = " + items.toString() + "]";
	 */
	@Override
	public String toString() {
		return this.getClass().getSimpleName() + ": [LimitX: " + this.getXLimit() + "], [LimitY: " + this.getYLimit() + "], [Total items: " + this.getNbItems() + "], " +
				"[Legal status: " + this.hasProperItems() + "], [Items: " + items.toString() + "]";
	}
	//TODO: waarom al die moeite om een hele klasse option te schrijven?????
		// ge moet toch gewoon als parameter voor de iterator een methode meegeven, die oproepen op een BoardItem en da resultaat gebruiken???
	// zie in klasse Option voor meer uitleg waarom ik dit een slechte oplossing vind.
	/**
	 * Returns an iterator which on the items on the board that match the given option.
	 * 
	 * @param option The option the items that will be pointed by this iterator must satisfy.
	 * @return 
	 * 		| {boardItem | boardItem == result.next() || boardItem == result.next() || ...} == {boardItem | getAllItemsType(BoardItem.class).contains(BoardItem) && option.satisfy(boardItem)}
	 * @throws NullPointerException
	 * 		When the option is null
	 * 		| option == null
	 */
	public Iterator<BoardItem> getItems(final SelectionOption option) throws NullPointerException {
		if(option == null)
			throw new NullPointerException("Board can not have null as selectionOption");
		return new Iterator<BoardItem>() {

			private final Iterator<BoardItem> it;
			/**
			 * The last element the iterator has returned.
			 * This element is needed to remember which item we must remove by the iterator.
			 */
			private BoardItem lastElement = null;
			
			/**
			 * Makes a Set<BoardItem> and fils it with the items that are on this board and that satisfy the option.
			 * it is made as an iterator on this Set.
			 */
			{
				Set<BoardItem> allItems = new HashSet<BoardItem>();
				for(Position lusPosition : getPositionsInUse())
					for(BoardItem lusBoardItem : getItemsAt(lusPosition))
						if(option.satisfy(lusBoardItem))
							allItems.add(lusBoardItem);
				it = allItems.iterator();
			}
			
			/**
			 * Controls if there is a next element the iterator can return.
			 */
			@Override @Basic
			public boolean hasNext() {
				return it.hasNext();
			}

			/**
			 * Returns the next element of sequence the iterator is pointing towards.
			 * 
			 * @throws NoSuchElementException
			 * 		When the iterator points to the last element.
			 * 		| !hasNext()
			 */
			@Override @Basic
			public BoardItem next() throws NoSuchElementException {
				lastElement = it.next();
				return lastElement;
			}

			/**
			 * Removes the current item the iterator is pointing towards from the board.
			 * 
			 * @effect The current item of which the iterator points towards (the previous item if it.next() is invoked) will be removed from the board.
			 * 		| let boardItem the previous element returned by it.next()
			 * 		| removeAsItem(boardItem)
			 * @throws IllegalStateException
			 * 		When the remove method is invoked multiple times without the method next()
			 * @throws IllegalStateException
			 * 		When the next() method is not yet invoked. Then the iterator will points to no element.
			 * @note If some other event changes the status of the board while iterator on it, there is no guarantee what
			 * will happen.
			 */
			@Override
			public void remove() throws IllegalStateException {
				try {
					it.remove();
					removeAsItem(lastElement);
				} catch(IllegalArgumentException exc) {
					//If in the mean time, the board is changed and the particular item is removed.
					//We solve this by removing nothing in that case.
				}
			}
			
		};
	}
}
